<?php
/**
 * @package php-font-lib
 * @link    https://github.com/PhenX/php-font-lib
 * @author  Fabien Ménager <fabien.menager@gmail.com>
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 */

namespace isszz\captcha\font\lib;

/**
 * Generic font file binary stream.
 *
 * @package php-font-lib
 */
class BinaryStream
{
	/**
	 * @var resource The file pointer
	 */
	protected $f;

	const uint8        = 1;
	const  int8        = 2;
	const uint16       = 3;
	const  int16       = 4;
	const uint32       = 5;
	const  int32       = 6;
	const shortFrac    = 7;
	const Fixed        = 8;
	const  FWord       = 9;
	const uFWord       = 10;
	const F2Dot14      = 11;
	const longDateTime = 12;
	const char         = 13;

	const modeRead      = "rb";
	const modeWrite     = "wb";
	const modeReadWrite = "rb+";

	public static function backtrace()
	{
		var_dump(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
	}

	/**
	 * Open a font file in read mode
	 *
	 * @param string $filename The file name of the font to open
	 *
	 * @return bool
	 */
	public function load($filename)
	{
		return $this->open($filename, self::modeRead);
	}

	/**
	 * Open a font file in a chosen mode
	 *
	 * @param string $filename The file name of the font to open
	 * @param string $mode     The opening mode
	 *
	 * @throws FontLibException
	 * @return bool
	 */
	public function open($filename, $mode = self::modeRead)
	{
		if (!in_array($mode, array(self::modeRead, self::modeWrite, self::modeReadWrite))) {
			throw new FontLibException("Unknown file open mode");
		}

		$this->f = fopen($filename, $mode);

		return $this->f != false;
	}

	/**
	 * Close the internal file pointer
	 */
	public function close()
	{
		return fclose($this->f) != false;
	}

	/**
	 * Change the internal file pointer
	 *
	 * @param resource $fp
	 *
	 * @throws FontLibException
	 */
	public function setFile($fp)
	{
		if (!is_resource($fp)) {
			throw new FontLibException('$fp is not a valid resource');
		}

		$this->f = $fp;
	}

	/**
	 * Create a temporary file in write mode
	 *
	 * @param bool $allow_memory Allow in-memory files
	 *
	 * @return resource the temporary file pointer resource
	 */
	public static function getTempFile($allow_memory = true)
	{
		$f = null;

		if ($allow_memory) {
			$f = fopen("php://temp", "rb+");
		}
		else {
			$f = fopen(tempnam(sys_get_temp_dir(), "fnt"), "rb+");
		}

		return $f;
	}

	/**
	 * Move the internal file pinter to $offset bytes
	 *
	 * @param int $offset
	 *
	 * @return bool True if the $offset position exists in the file
	 */
	public function seek($offset)
	{
		return fseek($this->f, $offset, SEEK_SET) == 0;
	}

	/**
	 * Gives the current position in the file
	 *
	 * @return int The current position
	 */
	public function pos()
	{
		return ftell($this->f);
	}

	public function skip($n)
	{
		fseek($this->f, $n, SEEK_CUR);
	}

	/**
	* @param int $n The number of bytes to read
	*
	* @return string
	*/
	public function read($n)
	{
		if ($n < 1) {
			return '';
		}

		return (string) fread($this->f, $n);
	}

	public function write($data, $length = null)
	{
		if ($data === null || $data === "" || $data === false) {
			return 0;
		}

		return fwrite($this->f, $data, $length);
	}

	public function readUInt8()
	{
		return ord($this->read(1));
	}

	public function readUInt8Many($count)
	{
		return array_values(unpack("C*", $this->read($count)));
	}

	public function writeUInt8($data)
	{
		return $this->write(chr($data), 1);
	}

	public function readInt8()
	{
		$v = $this->readUInt8();

		if ($v >= 0x80) {
			$v -= 0x100;
		}

		return $v;
	}

	public function readInt8Many($count)
	{
		return array_values(@unpack("c*", $this->read($count)));
	}

	public function writeInt8($data)
	{
		if ($data < 0) {
			$data += 0x100;
		}

		return $this->writeUInt8($data);
	}

	public function readUInt16()
	{
		// fix zz
		$a = @unpack("nn", $this->read(2));

		return $a ? $a["n"] : '';
	}

	public function readUInt16Many($count)
	{
		return array_values(unpack("n*", $this->read($count * 2)));
	}

	public function readUFWord()
	{
		return $this->readUInt16();
	}

	public function writeUInt16($data)
	{
		return $this->write(pack("n", $data), 2);
	}

	public function writeUFWord($data)
	{
		return $this->writeUInt16($data);
	}

	public function readInt16()
	{
		$a = unpack("nn", $this->read(2));
		$v = $a["n"];

		if ($v >= 0x8000) {
			$v -= 0x10000;
		}

		return $v;
	}

	public function readInt16Many($count)
	{
		$vals = array_values(unpack("n*", $this->read($count * 2)));
		foreach ($vals as &$v) {
			if ($v >= 0x8000) {
				$v -= 0x10000;
			}
		}

		return $vals;
	}

	public function readFWord()
	{
		return $this->readInt16();
	}

	public function writeInt16($data)
	{
		if ($data < 0) {
			$data += 0x10000;
		}

		return $this->writeUInt16($data);
	}

	public function writeFWord($data)
	{
		return $this->writeInt16($data);
	}

	public function readUInt32()
	{
		$a = unpack("NN", $this->read(4));
		return $a["N"];
	}

	public function writeUInt32($data)
	{
		return $this->write(pack("N", $data), 4);
	}

	public function readFixed()
	{
		$d  = $this->readInt16();
		$d2 = $this->readUInt16();

		return round($d + $d2 / 0x10000, 4);
	}

	public function writeFixed($data)
	{
		$left  = floor($data);
		$right = ($data - $left) * 0x10000;

		return $this->writeInt16($left) + $this->writeUInt16($right);
	}

	public function readLongDateTime()
	{
		$this->readUInt32(); // ignored
		$date = $this->readUInt32() - 2082844800;
		
		# PHP_INT_MIN isn't defined in PHP < 7.0
		$php_int_min = defined("PHP_INT_MIN") ? PHP_INT_MIN : ~PHP_INT_MAX;

		if (is_string($date) || $date > PHP_INT_MAX || $date < $php_int_min) {
			$date = 0;
		}

		return date("Y-m-d H:i:s", $date);
	}

	public function writeLongDateTime($data)
	{
		$date = strtotime($data);
		$date += 2082844800;

		return $this->writeUInt32(0) + $this->writeUInt32($date);
	}

	public function unpack($def)
	{
		$d = array();
		foreach ($def as $name => $type) {
			$d[$name] = $this->r($type);
		}

		return $d;
	}

	public function pack($def, $data)
	{
		$bytes = 0;
		foreach ($def as $name => $type) {
			$bytes += $this->w($type, $data[$name]);
		}

		return $bytes;
	}

	/**
	 * Read a data of type $type in the file from the current position
	 *
	 * @param mixed $type The data type to read
	 *
	 * @return mixed The data that was read
	 */
	public function r($type)
	{
		switch ($type) {
			case self::uint8:
				return $this->readUInt8();
			case self::int8:
				return $this->readInt8();
			case self::uint16:
				return $this->readUInt16();
			case self::int16:
				return $this->readInt16();
			case self::uint32:
				return $this->readUInt32();
			case self::int32:
				return $this->readUInt32();
			case self::shortFrac:
				return $this->readFixed();
			case self::Fixed:
				return $this->readFixed();
			case self::FWord:
				return $this->readInt16();
			case self::uFWord:
				return $this->readUInt16();
			case self::F2Dot14:
				return $this->readInt16();
			case self::longDateTime:
				return $this->readLongDateTime();
			case self::char:
				return $this->read(1);
			default:
				if (is_array($type)) {
					if ($type[0] == self::char) {
						return $this->read($type[1]);
					}
					if ($type[0] == self::uint16) {
						return $this->readUInt16Many($type[1]);
					}
					if ($type[0] == self::int16) {
						return $this->readInt16Many($type[1]);
					}
					if ($type[0] == self::uint8) {
						return $this->readUInt8Many($type[1]);
					}
					if ($type[0] == self::int8) {
						return $this->readInt8Many($type[1]);
					}
	  
					$ret = [];
					for ($i = 0; $i < $type[1]; $i++) {
				  		$ret[] = $this->r($type[0]);
					}
	  
					return $ret;
				}
	  
				return null;
		  }
	}

	/**
	 * Write $data of type $type in the file from the current position
	 *
	 * @param mixed $type The data type to write
	 * @param mixed $data The data to write
	 *
	 * @return int The number of bytes read
	 */
	public function w($type, $data)
	{
		switch ($type) {
		case self::uint8:
			return $this->writeUInt8($data);
		case self::int8:
			return $this->writeInt8($data);
		case self::uint16:
			return $this->writeUInt16($data);
		case self::int16:
			return $this->writeInt16($data);
		case self::uint32:
			return $this->writeUInt32($data);
		case self::int32:
			return $this->writeUInt32($data);
		case self::shortFrac:
			return $this->writeFixed($data);
		case self::Fixed:
			return $this->writeFixed($data);
		case self::FWord:
			return $this->writeInt16($data);
		case self::uFWord:
			return $this->writeUInt16($data);
		case self::F2Dot14:
			return $this->writeInt16($data);
		case self::longDateTime:
			return $this->writeLongDateTime($data);
		case self::char:
			return $this->write($data, 1);
		default:
			if (is_array($type)) {
				if ($type[0] == self::char) {
					return $this->write($data, $type[1]);
				}

				$ret = 0;
				for ($i = 0; $i < $type[1]; $i++) {
					if (isset($data[$i])) {
					$ret += $this->w($type[0], $data[$i]);
					}
				}

				return $ret;
			}
			return null;
		}
	}

	/**
	 * Converts a Uint32 value to string
	 *
	 * @param int $uint32
	 *
	 * @return string The string
	 */
	public function convertUInt32ToStr($uint32)
	{
		return chr(($uint32 >> 24) & 0xFF) . chr(($uint32 >> 16) & 0xFF) . chr(($uint32 >> 8) & 0xFF) . chr($uint32 & 0xFF);
	}
}
